/**
 * 对langChain工作流的封装：
 * 包括创建一个模型，构建prompt，新建session，获取历史记忆，模型请求，接收响应流等一系列抽象定义
 */
const { InMemoryChatMessageHistory } = require('@langchain/core/chat_history');
const { BaseMessage, MessageContent } = require('@langchain/core/messages');
const { ChatPromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder } = require('@langchain/core/prompts');
const { Runnable, RunnableWithMessageHistory } = require('@langchain/core/runnables');
const { z } = require('zod');

/**
 * 定义一个抽象类，用于提供基础模型
 */
class BaseModelProvider {
  /**
   * 定义一个静态属性，用于存储会话 ID 和历史记录的映射
   */
  static sessionIdHistoriesMap = {};

  /**
   * 定义一个静态方法，用于将答案内容转换为文本
   * @param {*} content
   * @returns
   */
  static answerContentToText(content) {
    // 如果内容是字符串，直接返回
    if (typeof content === 'string') {
      return content;
    }

    // 如果内容是数组，则遍历数组，将每个元素的文本拼接起来
    return content
      .map((c) => {
        if (c.type === 'text') {
          return c.text;
        }
        return '';
      })
      .join('');
  }

  /**
   * 定义一个异步方法，用于获取模型
   * @returns
   */
  async getModel() {
    // 如果模型不存在，则创建模型
    if (!this.model) {
      this.model = await this.createModel();
    }
    return this.model;
  }

  /**
   * 定义一个方法，用于创建提示，用于在 langChain 工作流中与模型进行交互
   * 创建prompt，如果有历史记录，根据历史记录创建，保持对话的上下文; 如果没历史记录根据用户输入的信息创建
   * @param {*} options
   * @returns
   *
   * 1. HumanMessagePromptTemplate (主要用于格式化和处理用户的输入消息)
      功能: 这个类主要用于定义人类用户输入的消息模板。它可以用来格式化用户的输入，使得输入更符合预期的格式。
      使用场景: 当你想要在与语言模型的交互中包含用户的输入信息时，HumanMessagePromptTemplate 可以帮助你指定输入的格式。例如，在一个对话中，用户可能会输入一段话，而这个类可以确保输入被正确地处理和展示。
      示例: 如果你使用 {input} 作为占位符，HumanMessagePromptTemplate 会在运行时将用户的实际输入填入这个位置。
     2. MessagesPlaceholder (用于在提示中占位历史消息，保持对话的上下文)
      功能: 该类用于在提示模板中占位历史消息。它允许在生成的提示中插入之前的对话记录，从而保持上下文的一致性。
      使用场景: 在对话系统中，历史消息通常是非常重要的，因为它们提供了上下文，帮助模型更好地理解当前的用户输入。通过使用 MessagesPlaceholder，你可以在提示中动态插入之前的对话内容。
      示例: 当 useHistory 参数为 true 时，MessagesPlaceholder 会被添加到提示中，表示在这个位置应该插入历史消息，确保模型能够看到之前的对话内容。

      */
  createPrompt(options) {
    // 获取选项，默认使用历史记录
    const { useHistory = true } = options ?? {};
    // 创建提示模板，包含历史记录占位符和人类消息模板
    const prompt = ChatPromptTemplate.fromMessages(
      [useHistory ? new MessagesPlaceholder('history') : '', HumanMessagePromptTemplate.fromTemplate('{input}')].filter(Boolean),
    );
    // 过滤空值: filter(Boolean) 用于移除数组中的空值。这确保了最终传递给 fromMessages 的数组中只包含有效的提示部分
    return prompt;
  }

  /**
   * 定义一个异步方法，用于获取历史记录
   * @param {*} sessionId
   * @param {*} appendHistoryMessages
   * @returns 返回会话 ID 对应的历史记录
   */
  async getHistory(sessionId, appendHistoryMessages) {
    // 如果会话 ID 的历史记录不存在，则创建新的历史记录
    if (BaseModelProvider.sessionIdHistoriesMap[sessionId] === undefined) {
      const messageHistory = new InMemoryChatMessageHistory();

      // 如果存在追加的历史记录消息，则添加到历史记录中
      if (appendHistoryMessages && appendHistoryMessages.length > 0) {
        await messageHistory.addMessages(appendHistoryMessages);
      }

      // 将会话 ID 和历史记录映射保存到静态属性中
      BaseModelProvider.sessionIdHistoriesMap[sessionId] = messageHistory;
    }
    return BaseModelProvider.sessionIdHistoriesMap[sessionId];
  }

  /**
   * Runnable
   * 在 langChain 库中，Runnable 是一个核心概念，用于表示可以执行的操作或链条。
   * 具体来说，Runnable 是一个抽象的接口或基类，
   * 允许你将多个操作（例如，模型调用、数据处理、提示生成等）组合在一起，以形成一个可执行的流程或链条。
   *
   * 可执行性: Runnable 是可以被执行的对象，通常会有一个 run 方法（或同等的执行方法），调用这个方法可以触发一系列操作。
   * 组合性: 你可以将多个 Runnable 对象组合在一起，形成一个链条（chain），这样可以方便地处理复杂的工作流。
   *         例如，你可以先生成提示，然后调用模型，最后处理输出。
   *
   *  与工作流的关系:
   *    Runnable 是构建工作流的基石。通过将不同的操作封装成 Runnable 对象，开发者可以轻松地创建复杂的交互模式。例如，在一个聊天机器人应用中，你可能需要：
        接收用户输入（Runnable）。
        生成合适的提示（Runnable）。
        调用语言模型进行推理（Runnable）。
        处理和格式化模型的响应（Runnable）。
        这些步骤可以通过链式调用多个 Runnable 对象来实现。
   *
   */

  /**
   * 创建带历史记录的 Runnable
   * 带历史记录的 Runnable: 允许你在执行时使用历史上下文，保持对话的连贯性。
   * @param {*} chain 一个可运行的链条，它通常是由提示和模型连接而成的。
   * @param {*} historyMessages 存储历史消息的数组，用于在运行时提供上下文
   * @returns
   */
  createRunnableWithMessageHistory(chain, historyMessages) {
    return new RunnableWithMessageHistory({
      runnable: chain,
      // 获取历史记录的回调函数
      getMessageHistory: async (sessionId) => await this.getHistory(sessionId, historyMessages),
      // 输入消息的键
      inputMessagesKey: 'input',
      // 历史消息的键
      historyMessagesKey: 'history',
    });
  }

  /**
   * 创建 Runnable
   * 基础 Runnable: 处理基本的操作。
   * @param {*} options
   * @returns 如果使用历史记录，则创建带历史记录的 Runnable
   */
  async createRunnable(options) {
    // 获取选项，默认使用历史记录
    const { useHistory = true, historyMessages = [], signal } = options ?? {};
    // 获取模型
    const model = await this.getModel();
    // 创建提示
    const prompt = await this.createPrompt({ useHistory });
    // 创建链，将提示和模型连接起来
    const chain = prompt.pipe(signal ? model.bind({ signal }) : model);
    return useHistory ? await this.createRunnableWithMessageHistory(chain, historyMessages || []) : chain;
  }

  /**
   * 创建结构化输出 Runnable
   * 结构化输出的 Runnable: 处理特定格式的输出，可能会包括数据验证或转换。
   * @param {*} options
   * @returns  如果使用历史记录，则创建带历史记录的 Runnable
   */
  async createStructuredOutputRunnable(options) {
    // 获取选项，默认使用历史记录
    const { useHistory = true, historyMessages = [], zodSchema } = options ?? {};
    // 获取模型
    const model = await this.getModel();
    // 创建提示
    const prompt = await this.createPrompt({ useHistory });
    // 创建链，将提示和模型连接起来
    const chain = prompt.pipe(model);
    return useHistory ? await this.createRunnableWithMessageHistory(chain, historyMessages || []) : chain;
  }
}

module.exports = {
  BaseModelProvider,
};
